﻿/*
	Copyright (C) 2012 Bernardo van der Wal <bernardo@thebigb.nl>

	Permission is hereby granted, free of charge, to any person obtaining a copy of
	this software and associated documentation files (the "Software"), to deal in
	the Software without restriction, including without limitation the rights to
	use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
	of the Software, and to permit persons to whom the Software is furnished to do
	so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in all
	copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
	SOFTWARE.
*/
using System;
using System.Collections.Generic;

namespace TheBigB.JsonSerializerLite
{
	[Flags]
	public enum ValidatorOptions
	{
		Quotes = 1,
		Braces = 2,
		Brackets = 4,
		ObjectNesting = 8,
		ArrayNesting = 16,
		Keys = 32,
		Values = 64,
		All = 127
	}

	public static class JsonValidator
	{
		private static List<char> allowedValueBeginnings = new List<char>(new char[] { ':', '[', ',' });
		private static List<char> allowedValueEndings = new List<char>(new char[] { '"', ']', '}' });
		private static List<string> allowedValueLiterals = new List<string>(new string[] { "true", "false", "null" });
		// ^(-?)([1-9]|0(?!\d))(\d*)(.?)(\d+)([eE][+-]?\d+)?$

		public static bool IsValidJSON(string json, ValidatorOptions options = ValidatorOptions.All, bool throwExceptions = true)
		{
			int quoteCount = 0;
			char? prev = ' ';

			json = json.Trim();

			// Count quotes (skip escaped ones)
			if (OptionEnabled(options, ValidatorOptions.Quotes))
			{
				foreach (char c in json)
				{
					if (c == '"' && prev != '\\')
						quoteCount++;
					prev = c;
				}

				// Quote count uneven?
				if ((quoteCount & 1) == 1)
				{
					if (throwExceptions)
						throw new InvalidJSONException("The provided JSON string has non-escaped or missing quotation marks");
					else
						return false;
				}
			}

			int braceDepth = 0, bracketDepth = 0;
			bool inString = false;
			string value = "";
			prev = null;

			// Count braces and brackets
			foreach (char c in json)
			{
				if(c == '"' && prev != '\\')
					inString = !inString;

				if(!inString)
				{
					switch (c)
					{
						// Object opened
						case '{':
							// Objects are only allowed as root, value or array item
							if (OptionEnabled(options, ValidatorOptions.ObjectNesting) && !IsValidValueBeginning(prev))
							{
								if (throwExceptions)
									throw new InvalidJSONException("Invalidly nested JSON object");
								else
									return false;
							}

							braceDepth++;
							break;
						// Object closed
						case '}':
							// Objects may only end after a number, string, boolean, null-value, closed array or closed object
							if (OptionEnabled(options, ValidatorOptions.ObjectNesting) && !IsValidValueEnding(prev, value))
							{
								if (throwExceptions)
									throw new InvalidJSONException("Invalidly nested JSON object");
								else
									return false;
							}
							braceDepth--;
							break;

						// Array opened
						case '[': 
							// Arrays are only allowed as root, value or array item
							if (OptionEnabled(options, ValidatorOptions.ArrayNesting) && !IsValidValueBeginning(prev))
							{
								if (throwExceptions)
									throw new InvalidJSONException("Invalidly nested JSON array");
								else
									return false;
							}

							bracketDepth++;
							break;
						// Array closed
						case ']':
							// Arrays may only end after a number, string, boolean, null-value, closed array or closed object
							if (OptionEnabled(options, ValidatorOptions.ObjectNesting) && !IsValidValueEnding(prev, value))
							{
								if (throwExceptions) 
									throw new InvalidJSONException("Invalidly nested JSON object"); 
								else 
									return false;
							}
							bracketDepth--;
							break;

						// Switch to value
						case ':':
							// A valid key is always a string so the previous character must be a quote
							if (OptionEnabled(options, ValidatorOptions.Keys) && prev != '"')
							{
								if (throwExceptions)
									throw new InvalidJSONException("Invalid key");
								else
									return false;
							}
							break;
					}

					if (OptionEnabled(options, ValidatorOptions.Keys) && prev == '{' && c != '"' && !Char.IsWhiteSpace((char)c))
					{
						if (throwExceptions)
							throw new InvalidJSONException("Invalid key");
						else
							return false;
					}
					/*
					if (OptionEnabled(options, ValidatorOptions.Keys) && prev == '[' && c != '"' && c != '[' && c != '{' && !Char.IsNumber((char)c) && !Char.IsWhiteSpace((char)c))
					{
						if (throwExceptions)
							throw new InvalidJSONException("Invalid key");
						else
							return false;
					}
					*/

					if (Char.IsNumber((char)c))
					{
						if (OptionEnabled(options, ValidatorOptions.Values) && prev == null && prev != ':' && prev != ',' && prev != '[')
						{
							if (throwExceptions)
								throw new InvalidJSONException("Invalidly numeric value");
							else
								return false;
						}
					}

					if(c == ',' || c == ']' || c == '}')
					{
						if (OptionEnabled(options, ValidatorOptions.Values) && !IsValidValueEnding(prev, value))
						{
							if (throwExceptions)
								throw new InvalidJSONException("Invalid value");
							else
								return false;
						}
						value = "";
					}

					if (Char.IsLetter(c))
						value += c;
				}

				// If the brace depth is smaller than zero, the object has exited the root element
				if (OptionEnabled(options, ValidatorOptions.Braces) && braceDepth < 0)
				{
					if (throwExceptions)
						throw new InvalidJSONException("Trying to close an unopened JSON object; there is probably an opening brace missing somewhere");
					else
						return false;
				}

				// If the bracket depth is smaller than zero, the array has exited the root element
				if (OptionEnabled(options, ValidatorOptions.Brackets) && bracketDepth < 0)
				{
					if (throwExceptions)
						throw new InvalidJSONException("Trying to close an unopened array; there is probably an opening bracket missing somewhere");
					else
						return false;
				}

				if (!inString && !Char.IsWhiteSpace(c))
					prev = c;
			}

			// If the final brace depth is larger than zero it means there is at least one object not closed
			if (OptionEnabled(options, ValidatorOptions.Braces) && braceDepth > 0)
			{
				if (throwExceptions)
					throw new InvalidJSONException("Unterminated JSON object; there is probably an closing brace missing somewhere");
				else
					return false;
			}

			// If the final bracket depth is larger than zero it means there is at least one array not closed
			if (OptionEnabled(options, ValidatorOptions.Brackets) && bracketDepth > 0)
			{
				if (throwExceptions)
					throw new InvalidJSONException("Unterminated array; there is probably an closing brace missing somewhere");
				else
					return false;
			}

			return true;
		}

		private static bool IsValidValueBeginning(char? prev)
		{
			return prev == null || allowedValueBeginnings.Contains((char)prev);
		}

		private static bool IsValidValueEnding(char? prev, string value)
		{
			return prev != null && (allowedValueEndings.Contains((char)prev) || allowedValueLiterals.Contains(value) || Char.IsNumber((char)prev));
		}

		private static bool OptionEnabled(ValidatorOptions options, ValidatorOptions option)
		{
			return (options & option) == option;
		}
	}
}
